import datetime
import filecmp
import time

from flask import Flask, render_template, redirect, url_for, session, request, flash
from model import LoginForm, RegisterForm, GetAdminForm, MakePostForm, MakeProblemForm, SubmitForm, Command
import os
import shutil
import zipfile
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

app.config['upload_folder'] = 'problem_data'
app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///identifier.sqlite"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = 'Hello World'

db = SQLAlchemy(app)


def get_time():
    res = datetime.datetime.now()
    ret = datetime.datetime(res.year, res.month, res.day, res.hour, res.minute, res.second)
    return ret


def run_judge(command):
    os.system(command)


class User(db.Model):
    __tablename__ = 'user'
    user_id = db.Column(db.Integer, primary_key=True)
    created = db.Column(db.DateTime, default=get_time())
    user_name = db.Column(db.String, nullable=False)
    password = db.Column(db.String, nullable=False)
    admin = db.Column(db.Integer, server_default='0')


class Post(db.Model):
    __tablename__ = 'post'
    post_id = db.Column(db.Integer, primary_key=True)
    post_number = db.Column(db.Integer, nullable=False)
    created = db.Column(db.DateTime, default=get_time())
    author_id = db.Column(db.Integer, nullable=False)
    title = db.Column(db.String, nullable=False)
    content = db.Column(db.String, nullable=False)


class Problem(db.Model):
    __tablename__ = 'problem'
    problem_id = db.Column(db.Integer, primary_key=True)
    problem_number = db.Column(db.Integer, nullable=False)
    created = db.Column(db.DateTime, default=get_time())
    author_id = db.Column(db.String, nullable=False)
    title = db.Column(db.String, nullable=False)
    content = db.Column(db.String, nullable=False)
    memory_limit = db.Column(db.Integer, nullable=False)
    time_limit = db.Column(db.Integer, nullable=False)


class Global(db.Model):
    __tablename__ = 'global'
    global_id = db.Column(db.Integer, primary_key=True)
    total_problems = db.Column(db.Integer, nullable=False)
    total_posts = db.Column(db.Integer, nullable=False)
    total_submissions = db.Column(db.Integer, nullable=False)


class Submission(db.Model):
    __tablename__ = 'submission'
    submission_id = db.Column(db.Integer, primary_key=True)
    created = db.Column(db.DateTime, default=get_time())
    problem_id = db.Column(db.Integer, nullable=False)
    author_id = db.Column(db.String, nullable=False)
    code = db.Column(db.String, nullable=False)
    memory_used = db.Column(db.Integer, nullable=False)
    time_used = db.Column(db.Integer, nullable=False)
    language = db.Column(db.String, nullable=False)
    status = db.Column(db.String, nullable=False)


db.create_all()


def get_suffix(filename):
    return os.path.splitext(filename)[-1]


def get_prefix(filename):
    return os.path.splitext(filename)[0]


def is_logged_in(f):
    def wrapper(*args, **kwargs):
        if 'logged_in' in session:
            return f(*args, **kwargs)
        else:
            flash('请先登录')
            return redirect(url_for('login'))

    return wrapper


def not_logged_in(f):
    def wrapper(*args, **kwargs):
        if 'logged_in' in session:
            flash('您已登录')
            return redirect(url_for('index'))
        else:
            return f(*args, **kwargs)

    return wrapper


def is_admin(f):
    def wrapper(*args, **kwargs):
        if 'admin' not in session:
            flash('您无操作权限')
            return redirect(url_for('dashboard'))
        else:
            return f(*args, **kwargs)

    return wrapper


@app.route('/')
def index():  # put application's code here
    posts = Post.query.all()
    return render_template('index.html', posts=posts, session=session)


@app.route('/login', methods=['GET', 'POST'], endpoint='login')
@not_logged_in
def login():
    form = LoginForm(request.form)
    if form.validate_on_submit():
        user_name = form.user_name.data
        password = form.password.data
        user = User.query.filter_by(user_name=user_name).first()
        if not user:
            flash('用户不存在', 'danger')
            return redirect(url_for('login'))
        if password == user.password:
            session['logged_in'] = True
            if user.admin:
                session['admin'] = True
            session['user_id'] = user.user_id
            session['user_name'] = user_name
            flash('登录成功')
            return redirect(url_for('dashboard'))
        else:
            flash("用户名或密码错误", 'danger')
            return redirect(url_for('login'))

    return render_template('login.html', form=form)


@app.route('/logout', endpoint='logout')
@is_logged_in
def logout():
    session.clear()
    flash("已经退出登录")
    return redirect(url_for('index'))


@app.route('/register', methods=['GET', 'POST'], endpoint='register')
@not_logged_in
def register():
    form = RegisterForm(request.form)
    if form.validate_on_submit():
        user_name = form.user_name.data
        password = form.password.data
        user = User.query.filter_by(user_name=user_name).first()
        if not user:
            new_user = User(user_name=user_name, password=password)
            db.session.add(new_user)
            db.session.commit()
            flash('注册成功，请重新登录')
            return redirect(url_for('login'))
        else:
            flash('用户名已存在')
            return redirect(url_for('register'))
    return render_template('register.html', form=form)


@app.route('/get_admin', methods=['GET', 'POST'], endpoint='get_admin')
@is_logged_in
def get_admin():
    form = GetAdminForm(request.form)
    user = User.query.get(session['user_id'])
    if user.admin == 1:
        flash('你已经是管理员了')
        return redirect(url_for('dashboard'))
    if form.validate_on_submit():
        invite_code = form.invite_code.data
        if invite_code == '865872723':
            User.query.filter_by(user_id=user.user_id).update({'admin': 1})
            session['admin'] = True
            db.session.commit()
            flash('成功注册管理员')
            return redirect(url_for('dashboard'))
        else:
            flash('邀请码错误，请重试')
            return redirect(url_for('get_admin'))
    return render_template('get_admin.html', form=form)


@app.route('/dashboard', endpoint='dashboard')
@is_logged_in
def dashboard():
    return render_template('dashboard.html', session=session)


@app.route('/posts/<int:post_id>', endpoint='show_post')
@is_logged_in
def show_post(post_id):
    post = Post.query.get(post_id)
    author_name = User.query.get(post.author_id).user_name
    return render_template('post.html', post=post, author_name=author_name)


# noinspection SqlWithoutWhere
@app.route('/make_post', methods=['GET', 'POST'], endpoint='make_post')
@is_logged_in
@is_admin
def make_post():
    form = MakePostForm(request.form)
    if request.method == 'POST':
        title = request.form['title']
        content = request.form['content']
        if not title or not content:
            pass
        else:
            flash('发布成功')
            total_posts = Global.query.get(1).total_posts
            new_post = Post(post_number=total_posts + 1, author_id=session['user_id'], title=title, content=content)
            db.session.add(new_post)
            Global.query.filter_by(global_id=1).update({'total_posts': total_posts + 1})
            db.session.commit()
            return redirect(url_for('dashboard'))
    return render_template('make_post.html', form=form)


@app.route('/post_update/<int:post_id>', methods=['GET', 'POST'], endpoint='post_update')
@is_logged_in
@is_admin
def post_update(post_id):
    post = Post.query.get(post_id)
    if not post:
        flash('文章不存在')
        return redirect(url_for('index'))
    if 'admin' not in session and post['author_id'] != session['user_id']:
        flash('您无操作权限')
        return redirect(url_for('dashboard'))
    if request.method == 'POST':
        title = request.form['title']
        content = request.form['content']
        if not title or not content:
            pass
        else:
            flash('发布成功')
            Post.query.filter_by(post_id=post_id).update({'title': title, 'content': content})
            db.session.commit()
            return redirect(url_for('show_post', post_id=post_id))
    form = MakePostForm(request.form)
    form.title.data = post.title
    form.content.data = post.content
    return render_template('post_update.html', form=form)


# noinspection SqlWithoutWhere
@app.route('/posts/<int:post_id>/delete', endpoint='post_delete')
@is_logged_in
def post_delete(post_id):
    post = Post.query.get(post_id)
    if not post:
        flash('文章不存在')
        return redirect(url_for('index'))
    if 'admin' not in session and post['author_id'] != session['user_id']:
        flash('您无操作权限')
        return redirect(url_for('dashboard'))
    post_number = Post.query.get(post_id).post_number
    total_posts = Global.query.get(1).total_posts
    Post.query.filter_by(post_id=post_id).delete()
    Global.query.filter_by(global_id=1).update({'total_posts': total_posts - 1})
    to_be_modified_posts = Post.query.filter(Post.post_number > post_number).all()
    for to_be_modified_post in to_be_modified_posts:
        to_be_modified_post.post_number -= 1
    db.session.commit()
    return redirect(url_for('index'))


@app.route('/problemset', endpoint='problemset')
@is_logged_in
def problemset():
    problems = Problem.query.all()
    return render_template('problemset.html', problems=problems)


@app.route('/problems/<int:problem_id>/delete', endpoint='problem_delete')
@is_logged_in
@is_admin
def problem_delete(problem_id):
    problem = Problem.query.get(problem_id)
    if not problem:
        flash('题目不存在')
        return redirect(url_for('index'))
    total_problems = Global.query.get(1).total_problems

    # 删除当前题目
    Problem.query.filter_by(problem_id=problem_id).delete()
    # 维护总题目数
    Global.query.filter_by(global_id=1).update({'total_problems': total_problems - 1})
    # 修改其他题目的题号
    to_be_modified_problems = Problem.query.filter(Problem.problem_number > problem.problem_number).all()
    for to_be_modified_problem in to_be_modified_problems:
        to_be_modified_problem.problem_number -= 1
    db.session.commit()
    for item in os.scandir(app.config['upload_folder']):
        number = int(item.name)
        if number > problem.problem_number:
            os.rename(os.path.join(app.config['upload_folder'], item.name),
                      os.path.join(app.config['upload_folder'], str(number - 1)))
        elif number == problem.problem_number:
            shutil.rmtree(os.path.join(app.config['upload_folder'], item.name))
    return redirect(url_for('problemset'))


@app.route('/problems/<int:problem_id>', methods=['GET', 'POST'], endpoint='show_problem')
@is_logged_in
def show_problem(problem_id):
    problem = Problem.query.get(problem_id)
    if request.method == 'POST':
        language = request.values.get('language')
        code = request.form['code']
        author_id = session['user_id']
        total_submissions = Global.query.get(1).total_submissions

        source = os.path.join('judge', str(total_submissions + 1) + '.' + language)
        target = os.path.join('judge', str(total_submissions + 1) + '.exe')
        answer = os.path.join('judge', str(total_submissions + 1) + '.txt')
        data_path = os.path.join(app.config['upload_folder'], str(problem.problem_number))
        time_used = 0
        memory_used = 0
        file = open(source, 'w')
        file.write(code)
        file.close()
        status = 'AC'
        if language == 'cpp':
            os.system('g++ -std=c++11 ' + source + ' -o ' + target)
            if not os.path.exists(target):
                status = 'CE'
            else:
                for item in os.scandir(data_path):
                    if get_suffix(item.name) == '.in':
                        data_in = os.path.join(data_path, item.name)
                        data_out = os.path.join(data_path, get_prefix(item.name) + '.out')
                        command = Command(target + '<' + data_in + '>' + answer, str(total_submissions + 1) + '.exe')
                        start = time.perf_counter()
                        command.run(timeout=problem.time_limit)
                        end = time.perf_counter()
                        duration = end - start
                        time_used += duration
                        if duration >= problem.time_limit:
                            status = 'TLE'
                            break
                        if not os.path.exists(answer) or not filecmp.cmp(answer, data_out):
                            status = 'WA'
                            break
        time_used = round(time_used, 3)
        new_submission = Submission(problem_id=problem_id, author_id=author_id, code=code, time_used=time_used,
                                    memory_used=memory_used, language=language, status=status)
        db.session.add(new_submission)
        Global.query.filter_by(global_id=1).update({'total_submissions': total_submissions + 1})
        db.session.commit()
        for item in os.scandir('judge'):
            if get_prefix(item.name) == str(total_submissions + 1):
                os.remove(os.path.join('judge', item.name))
        return redirect(url_for('show_submission', submission_id=total_submissions + 1))
    form = SubmitForm()
    return render_template('problem.html', problem=problem, form=form, session=session)


@app.route('/problem_update/<int:problem_id>', methods=['GET', 'POST'], endpoint='problem_update')
@is_logged_in
@is_admin
def problem_update(problem_id):
    problem = Problem.query.get(problem_id)
    if not problem:
        flash('问题不存在')
        return redirect(url_for('problemset'))
    form = MakeProblemForm()
    form.title.data = problem.title
    form.content.data = problem.content
    form.memory_limit.data = problem.memory_limit
    form.time_limit.data = problem.time_limit
    if request.method == 'POST':
        file = request.files['file']
        title = request.form['title']
        content = request.form['content']
        time_limit = request.form['time_limit']
        memory_limit = request.form['memory_limit']
        change_file = request.values.get('change_file')
        flag = False
        if change_file == 'Y':
            flag = True
        if not content:
            flash('填写题目描述')
            render_template('problem_update.html', form=form)
        if not flag and file:
            flash('需要勾选更改数据文件选项')
        elif (not file and flag) or (file and get_suffix(file.filename) != '.zip'):
            flash('上传合适的数据文件')
        else:
            Problem.query.filter_by(problem_id=problem_id).update(
                {'title': title, 'content': content, 'time_limit': time_limit, 'memory_limit': memory_limit})
            if flag:
                filename = str(Problem.query.get(problem_id).problem_number)
                path = os.path.join(app.config['upload_folder'], filename)
                shutil.rmtree(path)
                os.mkdir(path)
                file.save(os.path.join(path, filename + '.zip'))
                z_file = zipfile.ZipFile(os.path.join(path, filename + '.zip'), 'r')
                for single_file in z_file.namelist():
                    z_file.extract(single_file, path)
            flash('问题修改成功')
            return redirect(url_for('show_problem', problem_id=problem_id))
    return render_template('problem_update.html', form=form)


@app.route('/problem/post', methods=['GET', 'POST'], endpoint='make_problem')
@is_logged_in
@is_admin
def make_problem():
    form = MakeProblemForm()
    if request.method == 'POST':
        file = request.files['file']
        title = request.form['title']
        content = request.form['content']
        time_limit = request.form['time_limit']
        memory_limit = request.form['memory_limit']
        if not content:
            flash('填写题目描述')
            render_template('make_problem.html', form=form)
        if not file or get_suffix(file.filename) != '.zip':
            flash('上传合适的数据文件')
        else:
            total_problems = Global.query.filter_by(global_id=1).first().total_problems
            total_problems += 1
            Global.query.filter_by(global_id=1).update({'total_problems': total_problems})
            new_problem = Problem(author_id=session['user_id'], title=title, content=content, time_limit=time_limit,
                                  memory_limit=memory_limit, problem_number=total_problems)
            db.session.add(new_problem)
            db.session.commit()
            filename = str(total_problems)
            os.mkdir(os.path.join(app.config['upload_folder'], filename))
            path = os.path.join(app.config['upload_folder'], filename)
            file.save(os.path.join(path, filename + '.zip'))
            z_file = zipfile.ZipFile(os.path.join(path, filename + '.zip'), 'r')
            for single_file in z_file.namelist():
                z_file.extract(single_file, path)
            return redirect(url_for('problemset'))
    return render_template('make_problem.html', form=form)


@app.route('/submission/<int:submission_id>', endpoint='show_submission')
@is_logged_in
def show_submission(submission_id):
    submission = Submission.query.get(submission_id)
    problem = Problem.query.get(submission.problem_id)
    return render_template('submission.html', submission=submission, problem=problem)


@app.route('/submissions', endpoint='show_submissions')
@is_logged_in
def show_submissions():
    page = request.args.get('page', 1)
    paginate = Submission.query.order_by(Submission.submission_id.desc()).paginate(page=int(page), per_page=10)
    arr = []
    for submission in paginate.items:
        author_id = submission.author_id
        problem_id = submission.problem_id
        author_name = User.query.get(author_id).user_name
        problem_title = Problem.query.get(problem_id).title
        problem_number = Problem.query.get(problem_id).problem_number
        arr.append((author_name, problem_number, problem_title))
    total = len(paginate.items)
    return render_template('submissions.html', paginate=paginate, arr=arr, total=total)


if __name__ == '__main__':
    db.create_all()
    app.run()
